// Package zlib provides ...
package zlib

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"runtime"
	"strings"
	"time"
)

// After creates a function that invokes func once it's called n or more times
func After(n int, fn interface{}) func(args ...interface{}) []reflect.Value {
	// Catch programming error while constructing the closure
	mustBeFunction(fn)

	return func(args ...interface{}) []reflect.Value {
		n--
		if n < 1 {
			return unsafeInvokeFunc(fn, args...)
		}
		return nil
	}
}

// Before creates a function that invokes func once it's called less than n times
func Before(n int, fn interface{}) func(args ...interface{}) []reflect.Value {
	// Catch programming error while constructing the closure
	mustBeFunction(fn)
	var res []reflect.Value
	return func(args ...interface{}) []reflect.Value {
		if n > 0 {
			res = unsafeInvokeFunc(fn, args...)
		}
		if n <= 0 {
			fn = nil
		}
		n--
		return res
	}
}

// Fn is for curry function which is func(...interface{}) interface{}
type Fn func(...interface{}) interface{}

// Curry make a curry function
func (f Fn) Curry(i interface{}) func(...interface{}) interface{} {
	return func(values ...interface{}) interface{} {
		v := append([]interface{}{i}, values...)
		return f(v...)
	}
}

// Compose compose the functions from right to left
func Compose(fnList ...func(...interface{}) interface{}) func(...interface{}) interface{} {
	return func(s ...interface{}) interface{} {
		f := fnList[0]
		restFn := fnList[1:]

		if len(fnList) == 1 {
			return f(s...)
		}

		return f(Compose(restFn...)(s...))
	}
}

// Delay make the function execution after delayed time
func Delay(delay time.Duration, fn interface{}, args ...interface{}) {
	// Catch programming error while constructing the closure
	mustBeFunction(fn)

	time.Sleep(delay)
	invokeFunc(fn, args...)
}

// Debounced creates a debounced function that delays invoking fn until after wait duration have elapsed since the last time the debounced function was invoked.
func Debounced(fn func(), duration time.Duration) func() {
	// Catch programming error while constructing the closure
	mustBeFunction(fn)

	timer := time.NewTimer(duration)
	timer.Stop()

	Go(func() {
		for {
			select {
			case <-timer.C:
				go fn()
			}
		}
	})

	return func() { timer.Reset(duration) }
}

// Schedule invoke function every duration time, util close the returned bool chan
func Schedule(d time.Duration, fn interface{}, args ...interface{}) chan bool {
	// Catch programming error while constructing the closure
	mustBeFunction(fn)

	quit := make(chan bool)
	Go(func() {
		for {
			unsafeInvokeFunc(fn, args...)
			select {
			case <-time.After(d):
			case <-quit:
				return
			}
		}
	})

	return quit
}

const (
	// DefaultRetryTimes times of retry
	DefaultRetryTimes = 5
	// DefaultRetryDuration time duration of two retries
	DefaultRetryDuration = time.Second * 3
)

// RetryConfig is config for retry
type RetryConfig struct {
	context       context.Context
	retryTimes    uint
	retryDuration time.Duration
}

// RetryFunc is function that retry executes
type RetryFunc func() error

// Option is for adding retry config
type Option func(*RetryConfig)

// RetryTimes set times of retry
func RetryTimes(n uint) Option {
	return func(rc *RetryConfig) {
		rc.retryTimes = n
	}
}

// RetryDuration set duration of retries
func RetryDuration(d time.Duration) Option {
	return func(rc *RetryConfig) {
		rc.retryDuration = d
	}
}

// Context set retry context config
func Context(ctx context.Context) Option {
	return func(rc *RetryConfig) {
		rc.context = ctx
	}
}

// Retry executes the retryFunc repeatedly until it was successful or canceled by the context
// The default times of retries is 5 and the default duration between retries is 3 seconds
func Retry(retryFunc RetryFunc, opts ...Option) error {
	config := &RetryConfig{
		retryTimes:    DefaultRetryTimes,
		retryDuration: DefaultRetryDuration,
		context:       context.TODO(),
	}

	for _, opt := range opts {
		opt(config)
	}

	var i uint
	for i < config.retryTimes {
		err := retryFunc()
		if err != nil {
			select {
			case <-time.After(config.retryDuration):
			case <-config.context.Done():
				return errors.New("retry is cancelled")
			}
		} else {
			return nil
		}
		i++
	}

	funcPath := runtime.FuncForPC(reflect.ValueOf(retryFunc).Pointer()).Name()
	lastSlash := strings.LastIndex(funcPath, "/")
	funcName := funcPath[lastSlash+1:]

	return fmt.Errorf("function %s run failed after %d times retry", funcName, i)
}
